<template>
  <!-- demo2 -->
  <div class="index">
    <el-button
      type="warning"
      @click="showG6Dialog"
    >点击展示图谱弹窗</el-button>
    <!-- 图谱弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      class="g6-dialog"
      title="图谱弹窗"
      width="90%"
      @close="handleClose"
      :draggable="true"
    >
      <div
        class="container"
        id="container"
      ></div>
    </el-dialog>
  </div>
</template>

<script setup>
import { nextTick, onUnmounted, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import G6 from '@antv/g6';
import elementResizeDetectorMaker from 'element-resize-detector';

// 路由
const router = useRouter();

// 弹窗
const dialogVisible = ref(false);
// 模拟数据
const data = {
    "nodes": [{
      "id": "0",
      "label": "0"
    }, {
      "id": "1",
      "label": "1"
    }, {
      "id": "2",
      "label": "2"
    }, {
      "id": "3",
      "label": "3"
    }, {
      "id": "4",
      "label": "4"
    }, {
      "id": "5",
      "label": "5"
    }, {
      "id": "6",
      "label": "6"
    }, {
      "id": "7",
      "label": "7"
    }, {
      "id": "8",
      "label": "8"
    }, {
      "id": "9",
      "label": "9"
    }, {
      "id": "10",
      "label": "10"
    }],

    "edges": [{
      "source": "0",
      "target": "1"
    }, {
      "source": "0",
      "target": "2"
    }, {
      "source": "0",
      "target": "4"
    }, {
      "source": "0",
      "target": "5"
    }, {
      "source": "0",
      "target": "7"
    }, {
      "source": "2",
      "target": "3"
    }, {
      "source": "2",
      "target": "8"
    },
    {
      "source": "3",
      "target": "10"
    },
    {
      "source": "3",
      "target": "6"
    },
    {
      "source": "8",
      "target": "9"
    },
    {
      
    }
  ]
  };
// 图谱实例化对象
const graph = ref(null);

//元素的值为true时，表示节点当前是折叠状态，
// 为false时，表示节点当前是展开状态
const nodeExpandedState = ref([]);

onMounted(() => {
  data.nodes.forEach((node) => {
    nodeExpandedState.value[node.id] = false;
  })
})

/* 卸载钩子 */
onUnmounted(() => {
  if (graph.value) {
    // 清空数据（可省略）
    graph.value.clear();
    // 销毁画布
    graph.value.destroy();
    // 将画布实例对象置空
    graph.value = null;
  }
});

/* 开启弹窗 */
const showG6Dialog = () => {
  dialogVisible.value = true;
  // 防止未加载出弹窗DOM报错
  nextTick(() => {
    initGraph();
  });
};

/* 关闭弹窗 */
const handleClose = () => {
  if (graph.value) {
    // 清空数据（可省略）
    graph.value.clear();
    // 销毁画布
    graph.value.destroy();
    // 将画布实例对象置空
    graph.value = null;
  }
};
/* 初始化图谱 */
const initGraph = () => {
  // 节点添加提示框
  const tooltip = new G6.Tooltip({
    offsetX: 20, // tooltip 的 x 方向偏移值，需要考虑父级容器的 padding
    offsetY: -30, // tooltip 的 y 方向偏移值，需要考虑父级容器的 padding
    getContent(e) {
      const outDiv = document.createElement('div');
      outDiv.style.width = '180px';
      outDiv.innerHTML = `
          <ul>
            <li>节点IP：${e.item.getModel().id}</li>
          </ul>`;
      return outDiv;
    },
    itemTypes: ['node'] // 给node添加tooltip
  });

  // 存储节点折叠状态


// 存储被隐藏的节点和边的信息
const hiddenItems = {};

 // 注册自定义边，带有小球动画
 G6.registerEdge('edgeWithBall', {
      
  //afterDraw(cfg, group) - 这是G6的生命周期方法，在边绘制完成后执行，用于添加额外的自定义内容
  //cfg 包含边的配置信息
  //group 是边的图形容器，可以向其中添加新的图形元素
  afterDraw(cfg, group) {
        //获取已绘制的边的图形对象
        const shape = group.get('children')[0]; // 获取边的路径
        //获取边的起点坐标
        const startPoint = shape.getPoint(0);   // 起点坐标
        
        // 创建小球，默认不可见
        const ball = group.addShape('circle', {
          attrs: {
            x: startPoint.x,//设置小球的初始位置为边的起点
            y: startPoint.y,
            fill: '#F46649',
            r: 20,
            opacity: 1, //
          },
          name: 'edge-ball',
        });
        
        // 将小球和动画信息保存到边的数据中
        //cfg.ball = ball;
        group.get('edge').set('ball', ball);
        cfg.ballRunning = false;
        
        return shape;
      },
    }, 'line'), // 继承内置的line边类型


  graph.value = new G6.Graph({
    container: 'container', // String | HTMLElement，必须，容器 id 或容器本身
    width: document.getElementById('container').clientWidth, // Number，必须，图的宽度
    height: document.getElementById('container').clientHeight, // Number，必须，图的高度
    fitCenter: true, // 是否平移图使其中心对齐到画布中心
    plugins: [tooltip], // 添加tooltip
    // 画布配置
    modes: {
      default: ['drag-canvas', 'zoom-canvas', 'drag-node'], // 允许拖拽画布、放缩画布、拖拽节点
    },
    // 基本配置
    layout: {
      type: 'dagre',
      preventOverlap: true, // 防止节点重叠
      linkDistance: 180, // 指定边距离为150
      nodeSpacing: 85, // 防止重叠时节点边缘间距的最小值
      alpha: 1, // 当前的迭代收敛阈值
      alphaDecay: 0.05, // 迭代阈值的衰减率。范围 [0, 1]
    },
    // 默认节点配置
    defaultNode: {
      type: 'circle', // 节点类型
      size: [120, 46],
      style: {
        fill: '#F6FCFE', // 填充色
        stroke: '#2EA1FF', // 节点描边颜色
        shadowColor: 'rgba(78,89,105,0.3)', // 阴影颜色
        lineWidth: 1, // 描边宽度
        radius: 3, // 圆角
        shadowBlur: 2, // 阴影大小
      },
      // 文本配置
      labelCfg: {
        position: 'center',
        style: {
          fontSize: 14,
          fill: '#0282FF',
          fontWeight: 400,
        },
      },
    },

    // 默认边配置
    defaultEdge: {
      type: 'edgeWithBall', // 使用自定义边类型
      style: {
        stroke: '#2EA1FF',
        lineWidth: 1,
        // 边添加箭头
        endArrow: {
          path: G6.Arrow.triangle(4, 4, 2),
          d: 2,
          fill: '#2EA1FF',
        }
      },
      labelCfg: {
        autoRotate: true,
        refY: 12,
        style: {
          fontSize: 12,
          fill: '#1D2129',
        }
      },
    },
    // 边状态样式
    edgeStateStyles: {
      selected: {
        stroke: '#0282FF',
        shadowBlur: 0,
        'text-shape': {
          fill: "#0282FF",
          fontWeight: 600,
        }
      }
    },
    // 节点状态样式
    nodeStateStyles: {
      // 选中后样式
      selected: {
        fill: '#0282FF', // 填充色
        stroke: '#0282FF', // 节点描边颜色
        lineWidth: 1, // 描边宽度
        shadowColor: 'rgba(0,102,210,0.5)',
        'text-shape': {
          fill: "#ffffff"
        }
      },
      // 悬浮后样式
      active: {
        fill: '#CDEEFF', // 填充色
        stroke: '#2EA1FF', // 节点描边颜色
        lineWidth: 1, // 描边宽度
        shadowColor: 'rgba(78,89,105,0.3)',
        'text-shape': {
          fill: "#0282FF",
          fontWeight: 500,
        }
      }
    }
  });


  // // 添加节点间多条连线，贝塞尔曲线
  G6.Util.processParallelEdges(data.edges, 65);
  // 解决拖动产生残影问题
  graph.value.get('canvas').set('localRefresh', false);
  
  // 边添加点击事件
  //1.当用户点击一条边时，触发edge:click事件
  graph.value.on('edge:click', (evt) => {
    const { item } = evt;//2.首先获取被点击的边元素item

    //3.遍历图中所有的边，清除它们的所有状态
    graph.value.getEdges().forEach((edge) => {
      graph.value.clearItemStates(edge);
    });
    //4.遍历图中所有的节点，清除它们的所有状态
    graph.value.getNodes().forEach((node) => {
      graph.value.clearItemStates(node);
    });
    //5.将被点击的边设置为"selected"（选中）状态
    graph.value.setItemState(item, 'selected', true);
    // 6.高亮与该边相连的节点
    graph.value.setItemState(item['_cfg'].source, 'selected', true);//将边的源节点（source）设置为"selected"状态
    graph.value.setItemState(item['_cfg'].target, 'selected', true);//将边的目标节点（target）设置为"selected"状态
  });


  // 节点悬浮高亮
  graph.value.on('node:mouseover', (e) => {
    graph.value.setItemState(e.item, 'active', true);
  });


  // 节点鼠标移出后使tooltip消失并取消节点悬浮高亮
  graph.value.on('node:mouseout', (e) => {
    document.getElementsByClassName('g6-component-tooltip')[0].style.display = 'none';
    graph.value.setItemState(e.item, 'active', false);
  });

 //双击节点，扩展或隐藏节点
  graph.value.on('node:dblclick', (evt) => {
    const { item } = evt;
    console.log("节点信息:",item);
    console.log("id:",item._cfg.id);
    //getModel 获取数据模型,Item 图项类是 Node 、Edge 、Guide 的抽象类
    const model = item.getModel();
    //console.log("model:",model);
    const nodeId = model.id;

    // 如果节点是折叠状态（true），则展开
    if(nodeExpandedState.value[nodeId]){
      console.log("如果节点是折叠状态，则进行展开");
      showItems(nodeId);
      nodeExpandedState.value[nodeId] = false;
    }
    // 如果节点是展开状态（nodeExpandedState为false），则进行折叠
    else
    {
        hideItems(nodeId);
        // 设置节点为展开状态
        nodeExpandedState.value[nodeId] = true;
    }
      graph.value.layout();

    // 添加小球动画 - 找出所有以当前节点为源的边
    const edges = graph.value.getEdges().filter(edge => {
      const edgeModel = edge.getModel();
      return edgeModel.source === nodeId;
    });
    
  })

  graph.value.on('node:click', (evt) => {
    const { item } = evt;
    //getModel 获取数据模型,Item 图项类是 Node 、Edge 、Guide 的抽象类
    const model = item.getModel();
    //console.log("model:",model);
    const nodeId = model.id;

    // 添加小球动画 - 找出所有以当前节点为源的边
    const edges = graph.value.getEdges().filter(edge => {
      const edgeModel = edge.getModel();
      return edgeModel.source === nodeId;
    });
    
    // 为每条边添加小球动画
    edges.forEach(edge => {
      animateBall(edge);
    });

    graph.value.layout();
  })






  graph.value.data(data);
  graph.value.render();

  function showItems(nodeId,visted = new Set()){
    // 避免重复处理已访问的节点（防止循环引用导致的无限递归）
    if(visted.has(nodeId)) return;
    visted.add(nodeId);

    // 查找所有以当前nodeId为source的边
    const edges = graph.value.getEdges().filter(edge => {
      const edgeModel = edge.getModel();
      return edgeModel.source === nodeId;
    });

    // 显示这些边
    edges.forEach(edge=>{
      const edgeModel = edge.getModel();
      const targetNode = edgeModel.target;
      // 显示边
      graph.value.showItem(edge);
      // 显示目标节点
      graph.value.showItem(targetNode);

      // 确保hiddenItems中有存储当前节点的数组
      if (!hiddenItems[nodeId]) {
        hiddenItems[nodeId] = [];
      }
      hiddenItems[nodeId].push(edge);

      // 递归处理目标节点
      showItems(targetNode,visted);
    })


  }

  function hideItems(nodeId, visited = new Set()) {
    // 避免重复处理已访问的节点（防止循环引用导致的无限递归）
    if (visited.has(nodeId)) return;
    visited.add(nodeId);
    
    // 查找所有以当前nodeId为source的边
    const edges = graph.value.getEdges().filter(edge => {
      const edgeModel = edge.getModel();
      return edgeModel.source === nodeId;
    });
    
    // 隐藏这些边并存储到hiddenItems中
    edges.forEach(edge => {
      const edgeModel = edge.getModel();
      const targetNode = edgeModel.target;
      // 隐藏当前边
      graph.value.hideItem(edge);
      // 隐藏目标节点
      graph.value.hideItem(targetNode);
      
      // 确保hiddenItems中有存储当前节点的数组
      if (!hiddenItems[nodeId]) {
        hiddenItems[nodeId] = [];
      }
      hiddenItems[nodeId].push(edge);
      
      // 递归处理目标节点
      hideItems(targetNode, visited);
    });
  }

  // 小球动画函数
  function animateBall(edge) {
    const edgeShape = edge.getKeyShape();//获取边的主要图形对象（线条）
    const edgeLength = edgeShape.getTotalLength();//获取边的路径总长度
    const ball = edge.get('ball');//获取之前在自定义边中创建的小球对象
    
    if (!ball){
      console.log("小球不存在");
      return;
    }  // 如果找不到小球，则退出函数
    
    // 停止现有动画
    ball.stopAnimate();
    
    // 这段代码将小球移回边的起点，并设置为完全可见
    const startPoint = edgeShape.getPoint(0);
    ball.attr({
      x: startPoint.x,
      y: startPoint.y,
      opacity: 1,
    });
    
    // 创建小球沿边移动的动画
    ball.animate(
      (ratio) => {
        // 根据路径长度和当前比例计算位置
        const point = edgeShape.getPoint(ratio);
        return {
          x: point.x,
          y: point.y,
        };
      },
      {
        repeat: true,     // 循环播放
        duration: 2000,   // 动画时长
        easing: 'linear', // 线性动画
      }
    );
  }

  // 监听弹窗大小变化
  let erd = elementResizeDetectorMaker();
  // 监听弹窗不监听画布原因：使用该插件会给dom加position:relative的属性，画布加入relative导致画布节点tooltip错位
  erd.listenTo(document.getElementById("container"), (element) => {
    let width = element.clientWidth;
    let height = element.clientHeight;
    graph.value.changeSize(width, height);
    graph.value.fitCenter();
  });
}
</script>

<style lang="less" scoped>
.index {
  width: 100%;
  height: 100%;
  position: relative;
  h1 {
    position: absolute;
  }
  .el-button {
    position: absolute;
    top: 50px;
    margin: 0;
    &:last-of-type {
      top: 100px;
    }
  }
}
</style>
<style lang="less">
.g6-dialog {
  min-width: 40%;
  min-height: 400px;
  height: 600px;
  resize: both;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  .el-dialog__body {
    width: 100%;
    height: 0;
    flex: 1;
    box-sizing: border-box;
    padding: 0;
  }
  .container {
    width: 100%;
    height: 100%;
  }
}
</style>